import { _decorator, Component, Node, Vec3, Graphics, Color, UITransform } from 'cc';
import TileOccluder from './TileOccluder';

const { ccclass, property } = _decorator;

export const CHUNK_SIZE = 512;

type ChunkKey = string; // `${cx},${cy}`

interface ChunkData {
  node: Node;
  cx: number;
  cy: number;
  blocked: Array<{ tx: number; ty: number; w: number; h: number }>;
}

interface PendingLoad {
  key: ChunkKey;
  cx: number;
  cy: number;
}

@ccclass('ChunkManager')
export class ChunkManager extends Component {
  @property
  public tileSize = 64;

  @property
  public loadsPerFrame = 1;

  private loaded: Map<ChunkKey, ChunkData> = new Map();
  private pool: Node[] = [];
  private pendingLoads: PendingLoad[] = [];
  private pendingKeys: Set<ChunkKey> = new Set();
  private occluder: TileOccluder | null = null;

  update() {
    this.processLoadQueue();
  }

  public ensureAround(playerPos: Vec3, radiusChunks = 1): void {
    const centerCx = Math.floor(playerPos.x / CHUNK_SIZE);
    const centerCy = Math.floor(playerPos.y / CHUNK_SIZE);

    // Desired set
    const desired = new Set<ChunkKey>();
    for (let dy = -radiusChunks; dy <= radiusChunks; dy++) {
      for (let dx = -radiusChunks; dx <= radiusChunks; dx++) {
        const cx = centerCx + dx;
        const cy = centerCy + dy;
        const key = `${cx},${cy}`;
        desired.add(key);
        if (!this.loaded.has(key) && !this.pendingKeys.has(key)) {
          this.enqueueChunk(cx, cy, key);
        }
      }
    }

    // Unload far chunks (radius+1 as hysteresis)
    const maxDist = radiusChunks + 1;
    for (const [key, data] of Array.from(this.loaded.entries())) {
      const dx = Math.abs(data.cx - centerCx);
      const dy = Math.abs(data.cy - centerCy);
      if (dx > maxDist || dy > maxDist) {
        this.unloadChunk(key);
      }
    }

    // Drop pending loads that are no longer desired
    for (let i = this.pendingLoads.length - 1; i >= 0; i--) {
      const entry = this.pendingLoads[i];
      if (!desired.has(entry.key)) {
        this.pendingLoads.splice(i, 1);
        this.pendingKeys.delete(entry.key);
      }
    }
  }

  public getLoadedKeys(): string[] {
    return Array.from(this.loaded.keys());
  }

  public getLoadedChunks(): Array<{ cx: number; cy: number }> {
    const out: Array<{ cx: number; cy: number }> = [];
    for (const data of this.loaded.values()) {
      out.push({ cx: data.cx, cy: data.cy });
    }
    return out;
  }

  public attachOccluder() {
    this.occluder = this.findOccluder();
  }

  private enqueueChunk(cx: number, cy: number, key: ChunkKey) {
    this.pendingLoads.push({ cx, cy, key });
    this.pendingKeys.add(key);
  }

  private processLoadQueue() {
    if (!this.pendingLoads.length) return;
    const budget = Math.max(1, Math.floor(this.loadsPerFrame));
    let remaining = budget;
    while (remaining > 0 && this.pendingLoads.length) {
      const next = this.pendingLoads.shift()!;
      this.pendingKeys.delete(next.key);
      this.loadChunk(next.cx, next.cy, next.key);
      remaining--;
    }
  }

  private loadChunk(cx: number, cy: number, key: ChunkKey = `${cx},${cy}`) {
    const node = this.pool.pop() ?? new Node(`TileChunk_${cx}_${cy}`);
    node.name = `TileChunk_${cx}_${cy}`;
    node.setPosition(cx * CHUNK_SIZE, cy * CHUNK_SIZE, 0);
    if (!node.parent) this.node.addChild(node);
    // Graphics for debug drawing
    const ui = node.getComponent(UITransform) ?? node.addComponent(UITransform);
    ui.setContentSize(CHUNK_SIZE, CHUNK_SIZE);
    const g = node.getComponent(Graphics) ?? node.addComponent(Graphics);
    g.clear();
    // Draw chunk border
    g.strokeColor = new Color(80, 80, 80, 255);
    g.lineWidth = 1;
    g.rect(0, 0, CHUNK_SIZE, CHUNK_SIZE);
    g.stroke();

    const tilesPer = Math.max(1, Math.floor(CHUNK_SIZE / this.tileSize));
    // Generate a few obstacle rectangles in tile units
    const blocked: Array<{ tx: number; ty: number; w: number; h: number }> = [];
    const count = 2 + Math.floor(Math.random() * 3); // 2~4
    for (let i = 0; i < count; i++) {
      const w = 1 + Math.floor(Math.random() * Math.min(3, tilesPer));
      const h = 1 + Math.floor(Math.random() * Math.min(3, tilesPer));
      const tx = Math.floor(Math.random() * Math.max(1, tilesPer - w));
      const ty = Math.floor(Math.random() * Math.max(1, tilesPer - h));
      blocked.push({ tx, ty, w, h });
      // Draw visual block
      g.fillColor = new Color(120, 120, 120, 255);
      g.rect(tx * this.tileSize, ty * this.tileSize, w * this.tileSize, h * this.tileSize);
      g.fill();
    }

    const data: ChunkData = { node, cx, cy, blocked };
    this.loaded.set(key, data);
    // Sync to occluder
    this.syncChunkToOccluder(data, true);
    if (typeof console !== 'undefined' && console.debug) {
      console.debug('[CHUNK] load', key);
    }
  }

  private unloadChunk(key: ChunkKey) {
    const data = this.loaded.get(key);
    if (!data) return;
    // Remove occupancy first
    this.syncChunkToOccluder(data, false);
    // Recycle node
    if (data.node.parent) data.node.removeFromParent();
    const g = data.node.getComponent(Graphics);
    g?.clear();
    this.pool.push(data.node);
    this.loaded.delete(key);
    if (typeof console !== 'undefined' && console.debug) {
      console.debug('[CHUNK] unload', key);
    }
  }

  private syncChunkToOccluder(data: ChunkData, add: boolean) {
    const occ = this.occluder ?? (this.occluder = this.findOccluder());
    if (!occ) return;
    if (!add) {
      occ.clearChunk(data.cx, data.cy);
      return;
    }
    occ.clearChunk(data.cx, data.cy);
    for (const r of data.blocked) {
      for (let dx = 0; dx < r.w; dx++) {
        for (let dy = 0; dy < r.h; dy++) {
          occ.setBlocked(data.cx, data.cy, r.tx + dx, r.ty + dy, true);
        }
      }
    }
  }

  private findOccluder(): TileOccluder | null {
    return this.node.getComponent(TileOccluder) ?? this.node.parent?.getComponent(TileOccluder) ?? null;
  }
}

export default ChunkManager;
export function bootstrapChunkManager(manager: ChunkManager) {
  console.log('[CHUNK] bootstrap...');
  manager.attachOccluder();
}
